import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)
import java.util.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

/**
 * Write a description of class StackWorld here.
 * 
 * @author (Thomas KOBER)
 * @author (Dilyan GEORGIEV)
 * 
 * @version (1.0)
 */
public class StackWorld extends World implements CollisionObserver
{
    private static final int headerOffset = 50;
    
    // Random Generator
    private Random      rndGen;
    // Stack Man
    private StackMan    stackMan;
    // Game paused flag
    private boolean     paused;
    // Display Lives
    private GameStatus  livesStatus;
    // Display Score
    private GameStatus  scoreStatus;
    // Display Backpack Contents
    private GameStatus  backpackStatus;
    // Current Level
    private int         level;
    
    /**
     * Constructor for objects of class StackWorld.
     */
    public StackWorld()
    {   
        super(800, 600, 1); 
        
        this.paused     = false;
        this.rndGen     = new Random();
        this.level      = 1;
        this.stackMan   = StackMan.getInstance();
        this.stackMan.registerCollisionObserver(this);
        
        this.prepare();
        
    }

    /**
     * Returns whether or not the current game execution is paused.
     * @return true if current game is paused, false otherwise.
     */
    public boolean isPaused()
    {
        return this.paused;
    }
    
    /**
     * Pauses/Continues the execution of the current game.
     * @param paused true if game should be paused, false if it should be continued.
     */
    public void setPaused(boolean paused)
    {
        this.paused = paused;
    }
    
    /**
     * Overrides World.started()
     * <br />
     * Pauses current game if a MessageBox is being displayed.
     */
    public void started()
    {
        if (super.getObjects(MessageBox.class).size() > 0) {
            this.paused = true;
        }
    }
    
    /**
     * Prepare the world for the start of the program. That is: create the initial
     * objects and add them to the world.
     */
    private void prepare()
    {   
        this.buildLabyrinth();
        
        this.placeActors();
        
        this.displayWelcomeMessage();
        
        this.displayGameStatus();
    }
    
    /**
     * Build the Labyrinth.
     */
    private void buildLabyrinth()
    {
        // North Border
        this.drawHorizontalLine(15, headerOffset, 27, Brick.class);
         
        // Home Base
        super.addObject(new HomeBase(), 35, 100);
         
        // Obstacle 1
        this.drawHorizontalLine(124, 144, 2, SmallTile.class);
        this.drawHorizontalLine(124, 204, 2, SmallTile.class);
        this.drawVerticalLine(82, 121, 8, Brick.class);
         
        // Obstacle 2
        this.drawHorizontalLine(344, 144, 2, SmallTile.class);
        this.drawHorizontalLine(344, 204, 2, SmallTile.class);
        
        // Obstacle 3
        this.drawHorizontalLine(565, 88, 1, SmallTile.class);
        
        // Obstacle 4
        this.drawVerticalLine(703, 147, 2, SmallTile.class);
        
        // Obstacle 5
        this.drawVerticalLine(565, 206, 3, SmallTile.class);
         
        // Obstacle 6
        this.drawHorizontalLine(760, 325, 1, SmallTile.class);
        this.drawVerticalLine(679, 302, 4, Brick.class);
        this.drawVerticalLine(708, 302, 4, Brick.class);
         
        // Obstacle 7
        this.drawHorizontalLine(565, 446, 2, SmallTile.class);
        this.drawVerticalLine(699, 423, 4, Brick.class);
        this.drawVerticalLine(729, 423, 4, Brick.class); 
         
        // Obstacle 8
        this.drawHorizontalLine(616, 506, 1, SmallTile.class);
        this.drawVerticalLine(540, 483, 4, Brick.class);
        this.drawVerticalLine(568, 483, 4, Brick.class);
         
        // Obstacle 9
        this.drawHorizontalLine(760, 571, 1, SmallTile.class);
         
        // Obstacle 10 
        this.drawHorizontalLine(424, 571, 1, SmallTile.class);//Alligned this object with the rest in line. (Object is the middle 1 pinktile.
         
        // Obstacle 11
        this.drawVerticalLine(264, 451, 2, SmallTile.class);
        
        // Obstacle 12
        this.drawHorizontalLine(41, 571, 2, SmallTile.class);
        
        // Obstacle 13
        this.drawHorizontalLine(120, 446, 1, SmallTile.class);
        
        // Obstacle 14
        this.drawHorizontalLine(41, 325, 2, SmallTile.class);
        
        // Obstacle 15
        this.drawHorizontalLine(264, 331, 3, SmallTile.class);
        this.drawVerticalLine(424, 391, 2, SmallTile.class);
    }
    
    /**
     * Resets the position of StackMan.
     */
    private void resetStackManPossition() 
    {
        this.placeActorInWorld(stackMan);
    }
    
    /**
     * Places the Ghosts in the World.
     */
    private void placeGhosts()
    {
        // Place Enemies
        Ghost g1 = new Ghost();
        super.addObject(g1,493, 389);
        Ghost g2 = new Ghost();
        super.addObject(g2, 636, 239);
        Ghost g3 = new Ghost();
        super.addObject(g3, 497, 570);
    }
    
    /**
     * Places all Actors in the World.
     */
    private void placeActors()
    {
        Item1 item1 = new Item1();
        Item2 item2 = new Item2();
        Item3 item3 = new Item3();
        Item4 item4 = new Item4();
        Item5 item5 = new Item5();
        
        UltimateItem theItem = new UltimateItem();
        
        this.placeActorInWorld(item1);
        this.placeActorInWorld(item2);
        this.placeActorInWorld(item3);
        this.placeActorInWorld(item4);
        this.placeActorInWorld(item5);
        this.placeActorInWorld(theItem);
        
        // Place StackMan (always start at HomeBase)
        
        this.placeActorInWorld(stackMan);
        
        this.placeGhosts();
        //Ghost g4 = new Ghost();
        //super.addObject(g4, 113, 383);
    }
    
    /**
     * Draws a horizontal line with the given Obstacle subclass.
     * @param x x-coordinate of start of the line.
     * @param y y-coordinate of start of the line.
     * @param width length of the line.
     * @param clazz subclass of Obstacle with which the line should be drawn.
     */
    private void drawHorizontalLine(int x, int y, int width, Class<? extends Obstacle> clazz)
    {
        try {
            
            Obstacle newItem = clazz.newInstance();
            
            int itemWidth = newItem.getImage().getWidth();
            
            for (int i = 0; i < (width * itemWidth) ; i += itemWidth) {
                super.addObject(newItem, x + i, y);
                
                newItem = clazz.newInstance();
            }
        } catch (InstantiationException ex) { // TODO: Do Something in this case...
            ex.printStackTrace();
        } catch (IllegalAccessException ex) {
            ex.printStackTrace();
        }
    }
    
    /**
     * Draws a vertical line with the given Obstacle subclass.
     * @param x x-coordinate of start of the line.
     * @param y y-coordinate of start of the line.
     * @param height height of the line.
     * @param clazz subclass of Obstacle with which the line should be drawn.
     */
    private void drawVerticalLine(int x, int y, int height, Class<? extends Obstacle> clazz)
    {
        try {
            
            Obstacle newItem = clazz.newInstance();
            
            int itemHeight = newItem.getImage().getHeight(); 
            
            for (int i = 0; i < (height * itemHeight); i += newItem.getImage().getHeight()) {
                super.addObject(newItem, x, y + i);
                
                newItem = clazz.newInstance();
            }
        } catch (InstantiationException ex) { // TODO: Do Something in this case...
            ex.printStackTrace();
        } catch (IllegalAccessException ex) {
            ex.printStackTrace();
        }
    }
    
    /**
     * Randomly places an Actor in the World with respect to the Labyrinths constraints.
     * @param actor the actor to be placed in the world.
     */
    private void placeActorInWorld(Actor actor)
    {
        int x = rndGen.nextInt(super.getWidth());
        int y = rndGen.nextInt(super.getHeight());
        
        super.addObject(actor, x, y);
        
        while (!this.isActorWithinBounds(actor, x, y) || this.intersects(actor, Actor.class)) {
           
            super.removeObject(actor);
            
            x = rndGen.nextInt(super.getWidth());
            y = rndGen.nextInt(super.getHeight());
            
            super.addObject(actor, x, y);
        }        
    }
    
    /**
     * Check if the actor is within bounds.
     * @param actor the actor.
     * @param x x-coordinate of the actor.
     * @param y y-coordinate of the actor.
     * @return true if actor is within bounds, false otherwise.
     */
    private boolean isActorWithinBounds(Actor actor, int x, int y)
    {
        boolean inBounds = false;
        
        if (actor.getImage() != null) {
            boolean xInBounds = x >= actor.getImage().getWidth()                    && x <= super.getWidth()  - actor.getImage().getWidth();
            boolean yInBounds = y >= (actor.getImage().getHeight() + headerOffset)  && y <= super.getHeight() - actor.getImage().getHeight();
            
            inBounds = xInBounds && yInBounds;
        }
        
        return inBounds;
    }
    
    /**
     * Checks if the given actor intersects with another object of the given class.
     * @param actor the actor.
     * @param clazz class of objects with which the intersection check is being made.
     * @return true if actor intersects with another object, false otherwise.
     */
    private boolean intersects(Actor actor, Class clazz)
    {
        boolean intersects = false;
        
        if (actor instanceof SpecialItem) {
            SpecialItem item = (SpecialItem)actor;
            //intersects = (item.getOneIntersectingObject(clazz) != null);
            intersects = (item.getObjectsInRange(StackMan.BOUND_DISTANCE, clazz).size() > 0) || (item.getOneIntersectingObject(clazz) != null);
        } else if (actor instanceof StackMan) {
            StackMan man = (StackMan)actor;
            intersects = (man.getObjectsInRange(StackMan.BOUND_DISTANCE, clazz).size() > 0) || (man.getOneIntersectingObject(clazz) != null);
        }
        return intersects;
    }
    
    /**
     * Displays the current Game Status.
     */
    private void displayGameStatus()
    {
        this.updateLivesStatus();
        
        this.updateBackpackStatus();
        
        this.updateScore();
    }
    
    /**
     * Updates the Lives GameStatus property.
     */
    private void updateLivesStatus() 
    {
        Font font = new Font("Comic Sans MS", Font.PLAIN, 24);
        
        String text = "Lives: " + stackMan.getLives();
        Message lives = new Message(Color.BLACK, font, text, new Point(10, 35));
        
        if (this.livesStatus != null) {
            super.removeObject(this.livesStatus);
            this.livesStatus = null;
        } 
        
        this.livesStatus = new GameStatus(lives, 140, 40); 
        
        super.addObject(this.livesStatus, 64, 15);
    }
    
    /**
     * Updates the Score GameStatus property.
     */
    private void updateScore()
    {
        Font font = new Font("Comic Sans MS", Font.PLAIN, 24);
        
        String text = "Score: " + this.stackMan.getScore();
        Message score = new Message(Color.BLACK, font, text, new Point(10, 35));
        
        if (this.scoreStatus != null) {
            super.removeObject(this.scoreStatus);
            this.scoreStatus = null;
        } 
        
        this.scoreStatus = new GameStatus(score, 180, 40);
        
        super.addObject(this.scoreStatus, 580, 15);
    }
    
    /**
     * Updates the contents of the Backpack.
     */
    private void updateBackpackStatus()
    {
        Font font = new Font("Comic Sans MS", Font.PLAIN, 24);
        
        String text = "Backpack " + this.stackMan.getCurrentBackpackType() + ": ";
        Message backpackContent = new Message(Color.BLACK, font, text, new Point(10, 35));
        
        if (this.backpackStatus != null) {
            super.removeObject(this.backpackStatus);
            this.backpackStatus = null;
        }
        
        this.backpackStatus = new GameStatus(backpackContent, 170, 40);
        
        super.addObject(this.backpackStatus , 210, 15);
    }
    
    /**
     * Displays the Welcome Message.
     */
    private void displayWelcomeMessage()
    {
        ArrayList<Message> contents = new ArrayList<Message>();
        
        Color textColor = new Color(255, 204, 0);
        Font font = new Font("Comic Sans MS", Font.PLAIN, 24);
        String text = "\\m/o_O\\m/ STACK MAN \\m/o_O\\m/";
        
        contents.add(new Message(textColor, font, text, new Point(155, 70)));
        
        text = "Collect all crowns and drop them off at your homebase";
        
        contents.add(new Message(textColor, font, text, new Point(50, 150)));
        
        text = "in the correct order.";
        
        contents.add(new Message(textColor, font, text, new Point(50, 176)));
        
        text = "The correct order is smallest to largest and your";
        
        contents.add(new Message(textColor, font, text, new Point(50, 216)));
        
        text = "magic backpack works like a Queue (FIFO)!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 242)));
        
        text = "After dropping the items off, your magic backpack";
        
        contents.add(new Message(textColor, font, text, new Point(50, 282)));
        
        text = "will work like a Stack (LIFO)!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 308)));
        
        text = "Beware of all the haunted souls wandering around!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 348)));
        
        text = "Good Luck!!!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 428)));
        
        MessageBox box = new MessageBox(contents);
        super.addObject(box, 367, 298);
    }
    
    /**
     * Calculates the x-coordinate of the collected SpecialItem for display purposes.
     * @return next free x-coordinate for SpecialItem.
     */
    private int calcItemX()
    {
        return 309 + (35 * (this.stackMan.getBackpack().size() - 1));
    }
    
    /**
     * Displays the next Level Message, subject to success.
     * @param success true if all items have been collected in the correct order, false otherwise.
     */
    private void displayNextLevelMsg(boolean success)
    {
        ArrayList<Message> contents = new ArrayList<Message>();
        
        Color textColor = new Color(255, 204, 0);
        Font font = new Font("Comic Sans MS", Font.PLAIN, 24);
        String text = "\\m/o_O\\m/ STACK MAN \\m/o_O\\m/";
        
        contents.add(new Message(textColor, font, text, new Point(155, 70)));
        
        if (success) {
            text = "Excellent work!";
            
            contents.add(new Message(textColor, font, text, new Point(50, 150)));
            
            text = "You collected all items in the correct order!";
        } else {
            
            text = "Oh no!!";
            
            contents.add(new Message(textColor, font, text, new Point(50, 150)));
            
            text = "You didn't collect all items in the correct order!";
        }
        
        contents.add(new Message(textColor, font, text, new Point(50, 216)));
        
        if (this.stackMan.getBackpack() instanceof QueueBackpack) {
            text = "Your Magic Backpack now works like a Queue (FIFO)!";
        } else {
            text = "Your Magic Backpack now works like a Stack (LIFO)!";
        }
        
        contents.add(new Message(textColor, font, text, new Point(50, 242)));
        
        text = "Good Luck!!!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 428)));
        
        MessageBox box = new MessageBox(contents);
        super.addObject(box, 367, 298);
    }
    
    /**
     * Displays the Game Over Message.
     */
    private void displayGameOverMessage()
    {
        ArrayList<Message> contents = new ArrayList<Message>();
        
        Color textColor = new Color(255, 204, 0);
        Font font = new Font("Comic Sans MS", Font.PLAIN, 24);
        String text = "\\m/o_O\\m/ STACK MAN \\m/o_O\\m/";
        
        contents.add(new Message(textColor, font, text, new Point(155, 70)));
        
        text = "GAME OVER :-(!!!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 150)));
        
        text = "Unfortunately the haunted souls got you once too often!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 216)));
        
        text = "Click OK to try again!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 282)));
        
        text = "Good Luck!!!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 428)));
        
        MessageBox box = new MessageBox(contents);
        super.addObject(box, 367, 298);
    }
    
    /**
     * Displays the Life Lost (ie. StackMan collided with a Ghost) Message.
     */
    private void displayLostLifeMessage()
    {
        ArrayList<Message> contents = new ArrayList<Message>();
        
        Color textColor = new Color(255, 204, 0);
        Font font = new Font("Comic Sans MS", Font.PLAIN, 24);
        String text = "\\m/o_O\\m/ STACK MAN \\m/o_O\\m/";
        
        contents.add(new Message(textColor, font, text, new Point(155, 70)));
        
        text = "Oh no, they got you and you lost a life!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 150)));
        
        text = "Avoid crashing into Ghosts! We don't like that ;-)!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 216)));
        
        text = "Click OK to continue!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 282)));
        
        text = "Good Luck!!!";
        
        contents.add(new Message(textColor, font, text, new Point(50, 428)));
        
        MessageBox box = new MessageBox(contents);
        super.addObject(box, 367, 298);
    }
    
    /**
     * Resets the Game.
     */
    private void reset()
    {
       
        if (this.stackMan.getLives() > 0) {
            
            this.setPaused(true);
            super.removeObjects(super.getObjects(Ghost.class));
            this.placeGhosts();
            this.resetStackManPossition();
            this.displayLostLifeMessage();
        } else {
            this.setPaused(true);
            this.gameOver();
            this.displayGameOverMessage();
        }
        
    }
    
    /**
     * Executes the Games Game Over behaviour.
     */
    private void gameOver()
    {
        stackMan.initLives();
        stackMan.initScore();
        this.updateScore();
        this.updateLivesStatus();
        SpecialItem item = null;
        int size = this.stackMan.getBackpack().size();
        //This will empty the backpack.
        for (int i = 1; i <= size; i++) {
           item = (SpecialItem)this.stackMan.getBackpack().take();
        }
        
        //This will delete the old objects
        super.removeObjects(super.getObjects(SpecialItem.class));
        super.removeObjects(super.getObjects(Ghost.class));
        
        //this will reset the position of all the objects.
        this.placeActors();
    }
    
    /**
     * Executes the Games next Level behaviour.
     */
    private void nextLevel()
    {
        this.level++;
        
        if (level == 2) { // 1st level = queue backpack; 2nd level = stack backpack
            this.stackMan.setBackpack(new StackBackpack());
        } else {
            if (rndGen.nextInt(2) == 1) {
                this.stackMan.setBackpack(new QueueBackpack());
            } else {
                this.stackMan.setBackpack(new StackBackpack());
            }
        }
        
        this.updateBackpackStatus();
        
        // Delete old crowns from display
        super.removeObjects(super.getObjects(SpecialItem.class));
        super.removeObjects(super.getObjects(Ghost.class));
        
        this.placeActors();
    }
    
    // CollisionObserver Implementation
    
    /**
     * Handles StackMan - HomeBase Collisions
     */
    public void handleStackManHomeBaseCollision()
    {
        if (this.stackMan.getBackpack().isFull()) {
            
            SpecialItem item = null;
            boolean correctOrder = true;
            
            int size = this.stackMan.getBackpack().size();
            
            for (int i = 1; i <= size; i++) {
                item = (SpecialItem)this.stackMan.getBackpack().take();
                
                if (i == item.getItemValue()) {
                    this.stackMan.incrementScore(100);
                    this.updateScore();
                } else {
                    correctOrder = false;
                }
            }
            
            if (correctOrder) {
                this.stackMan.incrementScore(1000);
                this.updateScore();
            }
           
            this.setPaused(true);
            
            this.nextLevel();
            
            this.displayNextLevelMsg(correctOrder);
           
        }
    }
    
    /**
     * Handles StackMan - Ghost Collisions.
     */
    public void handleStackManGhostCollision()
    {
        this.stackMan.decrementLives();
        this.reset();
        this.updateLivesStatus();
    }
    
    /**
     * Handles StackMan - SpecialItem Collsisions.
     */
    public void handleStackManItemCollision(SpecialItem item)
    {
        super.removeObject(item);
        
        if (!(item instanceof UltimateItem)) {
            super.addObject(item, this.calcItemX(), 16);
            this.updateScore();
        }
    }
}
